Vert.x 为什么高性能

您所在的位置:网站首页 ftp 并发性能 Vert.x 为什么高性能

Vert.x 为什么高性能

2023-08-30 18:08| 来源: 网络整理| 查看: 265

前言

我们的故事从一只叫做Tom的小猫(Tomcat)开始,最初开发Java应用的Web服务器以Tomcat为主,它使用了同步阻塞式的I/O模型来处理请求,在这种模型下,当服务器接收到一个请求后,这个接收请求线程会去处理该请求,执行相关任务,如果它需要访问服务端I/O资源,那么调用者只有等待执行结果——这种方式引起的噩梦是:作为请求发送者的客户端程序需要一直等待结果。这似乎是我们最容易体会的“Hung现象”,虽然最终请求回来了,但总感觉不是那么顺畅,有些别扭。

2009年的某天,Node.js 觉醒了,就像一个开了挂的新武器在Linux社区发布,将一个古老的理论模型(姑且算古老)——事件驱动模型释放出来了,也许在Tomcat时代,没有人会想到事件驱动模型可以这么玩,而这个模型被 Node.js 成功应用于Web1(World Wide Web)中。也许它的初衷并不是为了Web应用,在Web 2.0中Ajax2(Asynchronous Javascript And XML)风靡的时代,人类从来都没有停止过将JavaScript应用到服务端程序的想法,Node.js就是这样一个角色,而且开启它的钥匙,也许是(真的也许)2008年Chrome浏览器中发布的V8引擎,由于这个引擎使得JavaScript执行效率大大提升,这把火使得JavaScript脚本逆袭了服务端。

随着Node.js的霸主地位飙升,也有更多的人发现了Node.js的不足:“人都是多疑善嫉的动物,看到完美的东西都喜欢鸡蛋里面挑骨头。 ”——越是曝晒在大众眼球中的东西,成长也会越快。

Node.js使用了JavaScript(后简称JS)脚本编写服务端程序,由于JS是一种对编程要求很高的脚本,懂得它很简单、可要精通它就变得很难,JS的可靠性并不高,所以使得Node.js在运行过程中,很容易因为程序本身的问题导致服务端死掉(有可能是一次数组越界)。 Node.js是单进程、单线程模式,且只支持单核CPU,它不能充分利用多核CPU服务器的优势,一旦这个进程崩掉,那么整个Web服务端就崩掉了——有点千里之堤毁于蚁穴的味道。 对于CPU密集型的应用,Node.js会不堪重负,主要原因也是由于JS是单线程的,如果存在长时间运行的计算,CPU的时间片无法在有效周期内释放,这使得后续的I/O请求无法继续发起。

  实际上Node.js上述的缺点是可以通过代码本身的健壮性来弥补的,但回到另外一个事实就是:真正掌握了JS这门语法的高手并不多(不要把懂当做精通),即使如此,这些缺陷无法掩盖Node.js作为某个时代霸主的事实:

它采用了事件驱动模型、纯异步编程,本身就是为了网络服务设计的,JS这种语言天生具有事件循环、闭包、匿名函数的特质,这些特质为事件驱动、异步编程提供了良好的土壤。 JS是一种上手极容易的语言,很多前端工程师也可以参与到服务端的开发中,它打通了前端和后端的芥蒂(语法一致性)。 Node.js采用了非阻塞的IO请求处理,使得相对紧缺的系统资源下同样可以发挥其高性能特质、以及负载能力,对于依赖其他IO装置的中间层,它是非常合适的。 Node.js非常轻量,且模块化方式很优雅(npm万能),当它面向分布式部署环境下的数据密集型应用时,堪称相对完美的解决方案。

在如今这种时代,移动网络、社交网络、电商成为了主流,使得服务提供商的客户端请求激增,这也是那只“小猫”显得虚疲的原因,这样的时代背景下,Node.js一枝独秀走在了最前边。扯了这么多,似乎和Vert.x没有任何关系,好的,那么让我们尝试在这样的背景下去解释Vert.x。

Eclipse Vert.x是Eclipse基金会下面的一个开源项目,Vert.x本身是事件驱动、非阻塞纯异步IO模型,这意味着可以使用很少的线程资源处理大量并发请求。

Vertx 实现了 Multi-Reactor 模式,区别于单线程的 Reactor 模式(Node.js实现了这种模式)。单一线程的问题在于它在任意时刻只能运行在一个核上, 如果你希望单线程(Reactor)应用(如你的 Node.js 应用)扩展到多核服务器上, 则需要启动并且管理多个不同的进程。Vert.x的工作方式有所不同。每个 Vertx 实例维护的是 多个Event Loop 线程。 默认情况下,会根据机器上可用的核数量来设置 Event Loop 的数量,也可自行设置。

这意味着 Vertx 进程能够在你的服务器上扩展,与 Node.js 不同。

可见Vert.x高性能的原因是支持让我们以异步非阻塞的方式来编写业务逻辑,且可以充分利用服务器的多核。

下面介绍一下同步阻塞编程与异步非阻塞编程的区别。

同步阻塞式编程

大多数应用程序和服务开发框架都基于多线程。 每个连接对应一个线程 ,开发者可以使用传统的 命令式 代码。

image.png

多个线程可以在同一个进程中存活,并行地执行工作,并共享同一片内存空间。

同步阻塞式编程存在的问题

当使用传统的阻塞式API做以下操作时,调用线程可能会被阻塞:

从 Socket 中读取数据 写数据到磁盘 发送消息给接收者并等待回复 其他很多情况

在上述情况下,线程在等待处理结果时它不能做任何事。

image.png

这意味着如果使用阻塞式API处理大量并发, 需要大量线程来防止应用程序停止运转。

而这些线程使用的内存(例如它们的栈)和线程上下文切换带来昂贵的开销。(一个线程大约占用 521KB-1M的内存空间,上下文切换大约在 2.7-5.48us 左右)

缺点如下:

工作负载过高,多线程处理这些请求会产生太多的上下文切换动作。 线程池中处于IO 阻塞的线程不能被复用(处理新的Http请求)。 需要解决多线程进行内存访问所存在的线程安全问题。 阻塞式的方式对于现代应用程序所需要的并发级别来说是难于扩展的。 Verx 异步非阻塞式编程

使用 Asynchronous I/O,可以用更小的线程处理更多的连接。 当一个任务发生了I/O操作时, Asynchronous I/O不会阻塞线程,而是执行其他待处理的任务,待到I/O结果准备好后再继续执行该任务。

Vert.x 使用 事件循环(event­loop) 多路复用并发处理工作负载。

image.png

在event loop 上运行的代码不应执行阻塞I/O及长时间的处理逻辑。所以 Vert.x有一个黄金法则:不要阻塞 event loop。

The Golden Rule - Don’t Block the Event Loop

思考:

具体等待多长的时间算是阻塞?它取决于你的应用程序和所需的并发数量。

如果你只有单个 Event Loop,而且你希望每秒处理10000个 HTTP 请求, 很明显的是每一个请求处理时间不可以超过0.1毫秒,所以你不能阻塞任何过多(大于0.1毫秒)的时间。

因为Vert.x API不会阻塞线程, 所以通过Vert.x,你可以只使用少量的线程来处理大量的并发。 例如,一个单独的 Event Loop 可以非常迅速地处理数千个 HTTP 请求。每个 Vertx 实例维护的是 多个Event Loop 线程 。 默认情况下,会根据机器上可用的核数量来设置 Event Loop 的数量。

异步非阻塞编程的缺点

编程思维的转变:采用同步命令式编程,符合人类的思维方式,而绝大多数异步编程,与我们人类思维上存在差异。所以异步编程的代码在阅读和理解上相比更困难:一方面思维上的不协调,而另一方面可维护性也不是很好。

上面之所以说:“绝大多数异步编程,与我们人类思维上存在差异”,是因为“协程“可以让我们继续用同步命令式的编程来编写异步代码(所以Koltin +Vert.x 是一个非常不错的选择)。另外,Vertx的编程范式基本都是函数式编程,所以也需要我们从 "面向对象编程->面向函数编程" 思维的转变。

Verx的最佳实践

客户端发起一个请求,服务器接收到请求后,通过eventbus将请求分发到具体Service里的某个方法,在此方法里进行查询数据库的异步操作,拿到异步结果后(Future/Promise/Coroutines),再通过event bus发送回一个响应,最终写回到客户端。整个操作流程都是异步的。

image.png

感兴趣的小伙伴,可以 clone 下来我的代码:代码仓库

响应式编程为什么没有成为主流

传统的 Java 语言以阻塞式编程并无问题,它仍是大多数团队与公司的第一选择。但非阻塞式的异步编程的优点也非常突出。

类似的响应式编程在性能上有极大的优势,但他一直未成为主流。我也在思考这个现象,总结出部分原因如下。

原因一:思维差异+可维护性差

这些年,响应式编程的概念很火,但事实上一直未能成为主流。响应式编程有着非常好的性能优势,非阻塞式的实现机制比阻塞式的实现机制确实好很多,但它仍有一个非常难以解决的问题,那就是:响应式编程带来的异步编程思想并不符合人类的思维。

人的思维是什么,我们理解一个事情的基本思维仍是面向对象及过程的。比如我们理解的上班是这样的:

先起床 乘坐交通工具去公司 早上开例会,安排任务 开始编码

如果就这件事,我们按照传统的面向对象及阻塞式的思维来编码,它是这样的:

// 这是伪代码 class Coder{ public void work(){ this.getUp() this.driverToOfficePlace() this.joinMeeting() this.code() } }

我们可以明显看到,这个编码与我们的思维是完全一致的,这就是我们所固有的习惯,阻塞式及同步的方式,是符合我们的思维的。

如果我们用一种响应式编程中的异步编程来实现,大致的代码可能是这样的:

// 这是伪代码 class Coder{ public void work(){ this.getUp().onSuccess(()->{ this.driveToOfficePlace(()->{ this.joinMeeting(()->{ this.code() }) }) }) } }

上面这个已经很简化了,事实上的业务不可能这么简洁,你可以想象当这个代码中充满各种代码细节时的场景。

大致上所有的异步编程都有这种风格,因为这种风格与我们人类思维上存在差异,所以有个非常著名的名字来称为它:回调地狱。

当然,写Java的可能对这个不太清楚,但前些年,使用Node.js的程序员对它可谓不所不知。事实上,移动端也一并存在类似的问题。

而且很明显,这种风格的代码在阅读与理解上相比更困难,这也是它可维护性较差的原因之一,也因此一并造成很多程序员写不好类似风格的代码,一方面思维上的不协调,而另一方面可维护性上也不是很好,而大多数公司和团队仍然有赖于大多数程序员的工作,这也是类似的编码模式一直未能成为主流的主要原因。

原因二:生态较差

如果我们说生态,那坦率的说,没有比Java语言生态更强大的语言了,这也是之所以这么多年,不喜欢Java的人非常多,但Java一直是后端的主力开发语言。 相比较而言,一些响应式的框架如果从生态上相比,就比Java差远了。类似RXJava等响应式编程语言,更多的是属于一个技术类库,其在生态上的不足也必然会阻碍一些程序员。

举例来说: 我如何用异步方式与数据库打交道?是否支持微服务?如何做OAUTH2权限?在Java的世界,你不需要为这些担忧,任何一个问题都有一大批成熟的解决方案。但在异步编程的世界,就相对差了很多。

"王者"到来: Vert.x

响应式编程存在的两个重要的问题,在 Vert.x 中,都得到了妥善解决,另外 Vert.x 还有绝对的性能优势。

改善了回调地狱的现象

前面所述的回调地狱问题,这个已经有较好的解决方案了。在 JS 的世界中,早已出现了 Promise 与 await 来解决这个问题。将非阻塞回调转成同步风格但实质还是非阻塞。

#TypeScript代码 public static async syncFavors(): Promise { //从网络获取星标联系人,这实质上是一个异步行为 const favors = await Contact.getNet().fetchFavorContacts(); if (favors) { //存储到数据库中,这也是一个异步操作 await Contact.getRespository().batchSaveFavors(favors); } return favors; }

虽然Vert.x本身未提供类似的功能,但Kotlin协程则提供了。基于它们的结合,就算是在异步编程中,你也可以如同前端TS一样,写出类似风格的代码:

@Test fun testExists(vertx:Vertx, testContext: VertxTestContext){ GlobalScope.launch { try { val user = User(username = "lingen",age = 35) //这是一个异步代码,但我们用await()来解决回调 val createdUser = repository.save(user).await() //这又是一个异步 var exists = repository.exists(User::class.java,createdUser.id).await() testContext.verify { Assertions.assertTrue(exists) } testContext.completeNow() }catch (e:Exception){ testContext.failNow(e) } } }

可以看出,Vert.x与Kotlin协程的结合,提供了类似的解决方案,使得我们在异步编程中,仍然能以符号人类思维的方式来编码。

较为完整的生态

类似的异步编程也好,响应式编程框架也好,在生态上都存在问题。表现为生态不够完善。 但这一点,在Vert.x反而是个优势。

Vert.x基本有自己的一整套生态,意味着你选择它,不用为项目中的任何一个维度的事情发愁,而且这些生态都是官方自己负责维护的,质量也比较有保证。

其在Web,数据库,单元测试,权限,微服务支持,消息事件机制,集群等有完整的解决方案。

image.png

如上图所示,Vert.x基本在每一方面都有自己的解决方案,这是非常明显的一个优势。

性能上的绝对优势

如果没有前两个优势,这个优势并不足以成为可以考量的优势。因为,我始终认为在绝大多数场景下,代码的可维护性是要放在第一重要的位置来考量的。

但如果有前两个优势,那这就成为另一个绝对优势了

在国外的性能大对比中,Vert.x始终处于前列。

image.png 而基于Spring Boot的实现,则弱于Vert.x数倍。

所以,综上所述,如果能写出简洁优雅的代码,生态又足够完善,又在性能上足够有优势。为什么不选择它呢?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3